#!/usr/bin/python2.6

# TODO: wire in the different event sinks

import collections
import time
import sys
import optparse

from clogger_flags import *

def make_field_filter(field_name, field_value):
  def filter_func(event, field_name=field_name, field_value=field_value):
    return getattr(event, field_name, None) == field_value
  return filter_func

class EventSink(object):
  def __init__(self, *event_types):
    self.interesting_event_types = event_types
    self.fast_filters = set()
    self.slow_filters = set()

  def Finalize(self):
    pass

  def AddFilter(self, attribute, term):
    self.fast_filters.add(term)
    self.slow_filters.add(make_field_filter(attribute, term))

  def UpdateFilters(self): pass

  def FastFilteredOut(self, line):
    for term in self.fast_filters:
      if line.find(term) < 0:
        return True

    return False

  def SlowFilteredOut(self, entry):
    for filter_func in self.slow_filters:
      if not filter_func(entry):
        return True

    return False

class PrintingSink(EventSink):
  mode_name = "print"
  mode_desc = "Print lines from the log file"

  def __init__(self, *args, **kwargs):
    EventSink.__init__(self, *args, **kwargs)
    self.last_event = 0
    self.fields = list()

  @classmethod
  def RegisterOptions(self, parser):
    parser.add_option("--relative", help="print relative timestamps", action="store_true")
    parser.add_option("--fields", help="magic format for output")

  def Feed(self, event):
    if self.options.fields and not self.fields:
      self.fields = self.options.fields.split()

    if self.options.relative:
      delta = event.epoch_time - self.last_event
      if delta > 86400: delta = 0   # reset after a day, or for first line
      print "%10.03f: %s" % (delta, event.raw_line_no_time)
      self.last_event = event.epoch_time
    else:
      if self.fields:
        print " ".join(str(getattr(event, f)) for f in self.fields)
      else:
        print event.raw_line

class UptimeSink(EventSink):
  mode_name = "uptime"
  mode_desc = "Buff/Debuff Uptime Sink"
  
  def __init__(self):
    EventSink.__init__(self, "SPELL_AURA_APPLIED", "SPELL_AURA_REMOVED")
    self._actor = None
    self._last_applied = None
    self._last_fade = None
    self._num_applies = 0
    self._uptime_duration = 0
    self._min_cooldown = 86400
    self._max_cooldown = 0
    self._min_duration = 86400
    self._max_duration = 0

  @classmethod
  def RegisterOptions(self, parser):
    parser.add_option("--ability", help="uptime of this ability")

  def Feed(self, event):
    if event.spell_name != self.options.ability: return
    if self._actor is None:
      self._actor = event.source_name
    elif self._actor != event.source_name:
      fatal("Uptime filter can only handle event for one actor at a time; "
            "received %s, but previously saw %s" % (event.source_name, self._actor))

    if event.type == "SPELL_AURA_APPLIED":
      if self._last_applied is not None:
        gap = event.epoch_time - self._last_applied
        self._min_cooldown = min(gap, self._min_cooldown)

      self._last_applied = event.epoch_time
      self._num_applies += 1

    if (event.type == "SPELL_AURA_REMOVED" and self._last_applied is not None):
      duration = event.epoch_time - self._last_applied
      self._uptime_duration += duration
      self._max_duration = max(duration, self._max_duration)
      self._min_duration = min(duration, self._min_duration)
      self._last_fade = event.epoch_time

  def Finalize(self):
    print "Uptime: %.2f seconds over %d applies; duration %.2f min, %.2f max; min cooldown %.2f" % \
          (self._uptime_duration, self._num_applies, self._min_duration,
           self._max_duration, self._min_cooldown)

class PushbackSink(EventSink):
  mode_name = "pushback"

  def __init__(self):
    EventSink.__init__(self)
    self._cast_starts = dict() # (spell, caster)->last_start
    self._cast_times = dict() # (spell, caster)->[time, time, ...]

  def Feed(self, event):
    if event.type == 'UNIT_DIED':
      bad_keys = list()
      for caster, spell in self._cast_starts:
        if caster == event.dest_name:
          bad_keys.append((caster, spell))
      for key in bad_keys:
        del self._cast_starts[key]
    elif event.prefix == 'SPELL' or event.prefix == 'SPELL_AURA':
      pushback_key = (event.source_name, event.spell_name)
      if event.type == 'SPELL_CAST_START':
        self._cast_starts[pushback_key] = event.epoch_time
      elif pushback_key in self._cast_starts:
        cast_time = event.epoch_time - self._cast_starts[pushback_key]
        self._cast_times.setdefault(pushback_key, []).append(cast_time)
        del self._cast_starts[pushback_key]

  def Finalize(self):
    keys = sorted(self._cast_times.keys())
    last_source = None
    for source, spell in keys:
      times = self._cast_times[(source, spell)]
      if last_source != source:
        print "%s:" % source
        last_source = source
      mean_time = sum(times) / len(times)
      print "  %-20s: %3d casts, %.2f avg (%.2f/%.2f max/min)" % \
            (spell, len(times), mean_time, max(times), min(times))

class MeleeDamageSink(EventSink):
  mode_name = "melee-damage"

  def __init__(self):
    EventSink.__init__(self)
    self._total_damage = dict() # source -> damage
    self._num_hits = dict()

  def Feed(self, event):
    if event.suffix == 'DAMAGE':
      who = event.dest_name
      self._num_hits[who] = self._num_hits.get(who, 0) + 1
      self._total_damage[who] = self._total_damage.get(who, 0) + int(event.amount)
      # histogram.Add(int(event.amount))

  def Finalize(self):
    for who in sorted(self._total_damage):
      print "%-20s: %d hits, %d damage, %.2f average" % \
            (who, self._num_hits[who], self._total_damage[who],
             self._total_damage[who] / self._num_hits[who])
      # histogram.Print()

class WhoDamagedWhatSink(EventSink):
  mode_name = "who-damaged"

  def __init__(self):
    EventSink.__init__(self)
    self._total_damage = dict() # source -> { vict: damage }

  def Feed(self, event):
    if event.suffix == 'DAMAGE' and event.SourceIsRaidMember():
      dst = event.dest_name
      src = event.source_name

      td = self._total_damage.get(src, None)
      if td is None:
        td = self._total_damage[src] = dict()

      td[dst] = td.get(dst, 0) + event.amount

  def Finalize(self):
    for src in sorted(self._total_damage):
      print "Damage dealt by %s" % src
      dst_list = sorted((dmg, vic) for vic, dmg in self._total_damage[src].items())
      for dmg, vic in sorted(dst_list, reverse=True):
        print "  %-20s: %d damage" % (vic, dmg)
      print

class ResistSink(EventSink):
  mode_name = "resists"

  def __init__(self):
    EventSink.__init__(self)
    self._player_spell_damage = dict() # player -> PlayerSpellDamage object

  def Feed(self, event):
    if hasattr(event, 'spell_name') and event.suffix in ('MISSED', 'DAMAGE'):
      key = (event.spell_name, event.dest_name)
      if key not in self._player_spell_damage:
        self._player_spell_damage[key] = PlayerSpellDamage(*key)
      dmg = self._player_spell_damage[key]
      if event.type == 'SPELL_MISSED':
        dmg.AddFullResist()
      elif event.type in ('SPELL_DAMAGE', 'SPELL_PERIODIC_DAMAGE'):
        dmg.AddPartialResist(int(event.amount), int(event.resisted))

  def Finalize(self):
    last_spell_name = None
    for (spell_name, _), dmg in sorted(self._player_spell_damage.items()):
      if spell_name != last_spell_name:
        last_spell_name = spell_name
        print "%s:" % spell_name

      rate = "(n/a)"
      if dmg.num_casts > 0:
        rate = "%.2f%%" % (100.0 * dmg.total_resists / dmg.num_casts)
      print "  %15s %6d dmg (%6d dmg resisted) (%3d total, %3d partial, " \
            "%3d full => %7s full resist rate)" % \
            (dmg.player, dmg.total_damage, dmg.total_resisted,
             dmg.num_casts, dmg.partial_resists, dmg.full_resists, rate)


class FirstDeathSink(EventSink):
  mode_name = "first-death"
  mode_desc = "First Death Sink"

  def __init__(self):
    EventSink.__init__(self)
    self._latest_event = 0
    self._saw_death = False
    self._combat_start = 0
    self._n = 0
    self._first_death_counts = dict()
    self._players_seen_this_fight = dict()
    self._player_attempt_totals = dict()

  @classmethod
  def RegisterOptions(self, parser):
    parser.add_option("", "--first_death", help="time of first death when fighting this mob")

  def Feed(self, event):
    if self.options.first_death in (event.source_name, event.dest_name):
      self._latest_event = event.epoch_time
      if self._combat_start == 0:
        self._combat_start = event.epoch_time

    if self._latest_event + 60 < event.epoch_time:
      for player in self._players_seen_this_fight:
        self._player_attempt_totals[player] = self._player_attempt_totals.get(player, 0) + 1
      self._saw_death = False
      self._combat_start = 0
      self._players_seen_this_fight = dict()

    if self._combat_start == 0: return

    if event.source_flags & COMBATLOG_OBJECT_TYPE_MASK == COMBATLOG_OBJECT_TYPE_PLAYER:
      self._players_seen_this_fight[event.source_name] = 1

    if (event.type == 'UNIT_DIED' and
        (event.dest_flags & COMBATLOG_OBJECT_TYPE_MASK) == COMBATLOG_OBJECT_TYPE_PLAYER and
        (event.dest_flags & COMBATLOG_OBJECT_AFFILIATION_MASK in
         (COMBATLOG_OBJECT_AFFILIATION_RAID,
          COMBATLOG_OBJECT_AFFILIATION_PARTY,
          COMBATLOG_OBJECT_AFFILIATION_MINE))):
      if not self._saw_death:
        self._saw_death = True
        self._n += 1
        self._first_death_counts[event.dest_name] = self._first_death_counts.get(event.dest_name, 0) + 1
        print "[%s] Attempt %d, first death: %s" % (time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self._combat_start)),
                                                    self._n,
                                                    event.dest_name)

  def Finalize(self):
    print
    print "Total attempts: %d" % self._n
    for player, deaths in sorted(self._first_death_counts.items()):
      attempts = self._player_attempt_totals[player]
      print "%-15s: %d deaths, %d attempts" % (player, deaths, attempts)

class ThorimTauntSink(EventSink):
  mode_name = "thorim-taunts"
  mode_desc = "Thorim taunting behavior"

  def __init__(self):
    EventSink.__init__(self, "SPELL_AURA_APPLIED", "SPELL_AURA_REMOVED", "SWING_DAMAGE", "SWING_MISSED", "UNIT_DIED")
    self._unbalanced = None
    self._unbalanced_time = 0

  def Feed(self, event):
    p = time.strftime("%m/%d %H:%M:%S", time.localtime(event.epoch_time))
    if event.epoch_time - self._unbalanced_time > 60:
      self._unbalanced = None
    if event.type == "SPELL_AURA_APPLIED" and event.spell_name == "Unbalancing Strike":
      self._unbalanced = event.dest_name
      print "%s.%03d Unbalanced: %s" % (p, int(1000 * event.epoch_time) % 1000, self._unbalanced)
      self._unbalanced_time = event.epoch_time
    elif event.type.startswith("SWING_") and event.source_name == "Thorim":
      if self._unbalanced == event.dest_name:
        if event.type == "SWING_DAMAGE":
          amount = event.amount + event.overkill
          if event.critical > 0:
            if event.overkill:
              hit_type = "CRIT_KILL"
            else:
              hit_type = "CRIT"
          else:
            if event.overkill:
              hit_type = "HIT_KILL"
            else:
              hit_type = "Hit"
          print "%s.%03d   %s while unbalanced: %s for %d" % (p, int(1000 * event.epoch_time) % 1000, hit_type, self._unbalanced, amount)
        else:
          hit_type = "missed"
          print "%s.%03d   missed while unbalanced: %s" % (p, int(1000 * event.epoch_time) % 1000, self._unbalanced)
      else:
        if self._unbalanced:
          delta = event.epoch_time - self._unbalanced_time
          if delta < 60:
            delta = " (%.2f seconds)" % delta
          else:
            delta = ""
            
          print "%s.%03d Thorim taunted by %s%s" % (p, int(1000 * event.epoch_time) % 1000, event.dest_name, delta)
          self._unbalanced = None
    elif (event.type == 'UNIT_DIED' and
          (event.dest_flags & COMBATLOG_OBJECT_TYPE_MASK) == COMBATLOG_OBJECT_TYPE_PLAYER and
          (event.dest_flags & COMBATLOG_OBJECT_AFFILIATION_MASK in
           (COMBATLOG_OBJECT_AFFILIATION_RAID,
            COMBATLOG_OBJECT_AFFILIATION_PARTY,
            COMBATLOG_OBJECT_AFFILIATION_MINE))):
      print "%s.%03d DEAD %s" % (p, int(1000 * event.epoch_time) % 1000, event.dest_name)


class VezaxSink(EventSink):
  mode_name = "vezax"
  mode_desc = "Vezax raid performance"

  def __init__(self):
    EventSink.__init__(self, "SPELL_AURA_APPLIED", "SPELL_AURA_REMOVED", "SPELL_HEAL", "SPELL_DAMAGE", "SPELL_MISSED")
    self._current_mark = None
    self._sc_hits = collections.defaultdict(int)
    self._mark_damage = collections.defaultdict(int)
    self._mark_heals = collections.defaultdict(int)
    self._mark_ticks = collections.defaultdict(int)
    self._mark_received = collections.defaultdict(int)
    self._mark_damage_time = list()
    self._mark_applied_time = 0
    self.fast_filters.add("General Vezax")

  def Feed(self, event):
    if (event.dest_name != "General Vezax" and
        not event.SourceIsRaidMember(pets=False) and
        not event.DestIsRaidMember(pets=False)):
      return
    if event.type == "SPELL_AURA_APPLIED" and event.spell_name == "Mark of the Faceless":
      self._current_mark = event.dest_name
      self._mark_received[self._current_mark] += 1
      self._mark_applied_time = event.epoch_time
    elif event.type == "SPELL_AURA_REMOVED" and event.spell_name == "Mark of the Faceless":
      self._current_mark = None
    elif event.type == "SPELL_HEAL" and event.spell_name == "Mark of the Faceless":
      self._mark_heals[self._current_mark] += event.amount
      if self._mark_applied_time != 0:
        self._mark_damage_time.append(event.epoch_time - self._mark_applied_time)
        self._mark_applied_time = 0
    elif event.type == "SPELL_DAMAGE" and event.spell_name == "Mark of the Faceless":
      self._mark_damage[self._current_mark] += event.amount + event.absorbed
      self._mark_ticks[self._current_mark] += 1
      if self._mark_applied_time != 0:
        self._mark_damage_time.append(event.epoch_time - self._mark_applied_time)
        self._mark_applied_time = 0
    elif event.type == "SPELL_MISSED" and event.spell_name == "Mark of the Faceless":
      self._mark_damage[self._current_mark] += event.amount
      self._mark_ticks[self._current_mark] += 1
    elif event.type == "SPELL_DAMAGE" and event.spell_name == "Shadow Crash" and event.source_name == "General Vezax":
      self._sc_hits[event.dest_name] += 1
      
  def Finalize(self):
    print "Shadow Crash hits:"
    events = sorted((v, k) for (k, v) in self._sc_hits.items())
    for damage, vict in events:
      print "  %-15s: %d" % (vict, damage)
    print
    print "Mark of the Faceless:"
    for player, count in sorted(self._mark_received.items()):
      print ("  %-15s: %5.2f ticks per hit, %3d marks, "
             "%3d ticks, %7d heals, %7d damage" % (player, 1.0 * self._mark_ticks[player] / count,
                                                   count, self._mark_ticks[player],
                                                   self._mark_heals[player], self._mark_damage[player]))

    print
    print "Mark damage times: %s" % " ".join(sorted("%.2f" % v for v in self._mark_damage_time))
    

class GrimReaperSink(EventSink):
  mode_name = "reaper"
  mode_desc = "Player Death Reports (like GrimReaper)"

  def __init__(self):
    EventSink.__init__(self, *("UNIT_DIED SPELL_HEAL SPELL_PERIODIC_HEAL DAMAGE_SHIELD "
                               "RANGE_DAMAGE SWING_DAMAGE SPELL_PERIODIC_DAMAGE SPELL_DAMAGE".split()))
    self._player_events = dict()  # player_name -> [ recent_events ]

  def Feed(self, event):
    now = event.epoch_time
    if event.DestIsRaidMember():
      player = event.dest_name
      events = list(e for e in self._player_events.get(player, []) if now - e.epoch_time < 60)
      events.append(event)
      if event.type == "UNIT_DIED":
        self.ReportEvents(player, events)
        events = list()
      self._player_events[player] = events

  def ReportEvents(self, player, events):
    pretty_time = events[-1].PrettyTimestamp()
    death_time = events[-1].epoch_time
    print
    print "%s   Death Report for %s" % (pretty_time, player)
    for event in events:
      delta = death_time - event.epoch_time
      source = event.source_name
      if source == "nil": source = "(none)"
      if hasattr(event, "absorbed"):
        if event.absorbed:
          absorbed = " (%d absorbed)" % event.absorbed
        else:
          absorbed = ""
      if event.suffix == "HEAL":
        if event.overheal < event.amount:
          print "%18.3f   %7d %s %s" % (delta, event.amount, source, event.spell_name)
      elif event.prefix == "SWING":
        print "%18.3f   %7d %s melee%s" % (delta, -event.amount, source, absorbed)
      else:
        if event.suffix == "DAMAGE":
          if event.type.startswith("SPELL"):
            resisted_percent = 100.0 * event.resisted / event.amount
            print "%18.3f   %7d %s %s (%.1f%% resisted)%s" % (delta, -event.amount, source, event.spell_name, resisted_percent, absorbed)
          else:
            print "%18.3f   %7d %s %s%s" % (delta, -event.amount, source, event.spell_name, absorbed)
        else:
          event_time = event.PrettyTimestamp()
          print "%s   %s" % (event_time, event.type)

class DamageStatsSink(EventSink):
  mode_name = "damage-stats"
  mode_desc = "Damage Stats Sink"

  def __init__(self):
    EventSink.__init__(self,
                       "RANGE_DAMAGE", "SPELL_PERIODIC_DAMAGE",
                       "SWING_DAMAGE", "SPELL_DAMAGE")
    self._uid_map = dict()   # uid -> name
    self._sources = dict()   # source_uid -> [ (name, time, dmg), (name, time, dmg), ... ]
    self._all_friendly_damage_events = list()
    self._heroism_events = list()

  @classmethod
  def RegisterOptions(self, parser):
    parser.add_option("", "--output-directory", help="output directory for cumulative damage stats")
    parser.add_option("-i", "--interval", help="report damage done per interval")

  def Feed(self, event):
    if event.type == "SPELL_AURA_APPLIED":
      if event.spell_name == 'Heroism':
        self._heroism_events.append((event.epoch_time, 1))
    if event.type == "SPELL_AURA_REMOVED":
      if event.spell_name == 'Heroism':
        self._heroism_events.append((event.epoch_time, -1))
    elif event.prefix == "SPELL":
      spell_name = event.spell_name
    elif event.prefix == "RANGE":
      spell_name = "RANGE_DAMAGE"
    elif event.prefix == "SWING":
      spell_name = "SWING_DAMAGE"
    else:
      return

    target_is_npc = (event.dest_flags & 0x800) == 0x800
    source_is_raid = (event.source_flags & 0x7) != 0
    if not target_is_npc or not source_is_raid: return

    if event.source_id not in self._uid_map:
      self._uid_map[event.source_id] = event.source_name
    if event.dest_id not in self._uid_map:
      self._uid_map[event.dest_id] = event.dest_name

    if event.source_id not in self._sources:
      self._sources[event.source_id] = list()

    self._sources[event.source_id].append((event.epoch_time, spell_name, event.amount, event.dest_id))
    self._all_friendly_damage_events.append((event.epoch_time, event.source_id, spell_name, event.amount, event.dest_id))

  def Finalize(self):
    last_damage = 0
    chart_xaxis = list()
    chart_yaxis = list()
    heroism_changes = list()
    next_print = 0
    accumulated_for_print = 0
    for timestamp, source_id, spell_name, amount, dest_id in self._all_friendly_damage_events:
      pretty_timestamp = time.strftime("%m/%d %H:%M:%S", time.localtime(timestamp))
      if timestamp - last_damage > 60:
        dps_window = list()
        if chart_xaxis and chart_yaxis:
          self.FinalizeChart(chart_xaxis, chart_yaxis, accumulated)
        accumulated = dict()
        chart_xaxis = list()
        chart_yaxis = list()

        if next_print > 0:
          p = time.strftime("%m/%d %H:%M:%S", time.localtime(last_damage))
          print "%s.%03d %d" % (p, int(1000 * timestamp) % 1000, accumulated_for_print)
        print "- [ New Fight ] -"
        next_print = 0
        accumulated_for_print = 0

      accumulated[dest_id] = accumulated.get(dest_id, 0) + amount
      accumulated_for_print += amount
      dps_window.append((timestamp, amount))
      for i in range(len(dps_window)):
        if timestamp - dps_window[i][0] < 5:
          break
      dps_window = dps_window[i:]
      if len(dps_window) > 1 and (dps_window[-1][0] - dps_window[0][0]) > 0:
        window_average = sum(r[1] for r in dps_window) / max(1, dps_window[-1][0] - dps_window[0][0])
      else:
        window_average = 0

      last_damage = timestamp
      source = self._uid_map[source_id]
      dest = self._uid_map[dest_id]
      if self.options.interval > 0:
        if next_print <= last_damage:
          print "%s.%03d %d" % (pretty_timestamp, int(1000 * timestamp) % 1000, accumulated_for_print)
          next_print = timestamp + self.options.interval
      else:
        print "%.2f %s %s %d %s (%d - %.2f)" % (timestamp, source, spell_name, amount, dest, accumulated[dest_id], window_average)
      chart_xaxis.append(timestamp)
      chart_yaxis.append(window_average)

    if chart_xaxis and chart_yaxis:
      self.FinalizeChart(chart_xaxis, chart_yaxis, accumulated)

    if 0:
      for uid, sources in self._sources.items():
        print "%s:" % self._uid_map[uid]
        for timestamp, damage_name, amount, dest_uid in sources:
          target = self._uid_map[dest_uid]
          print "  %.2f %s %d %s" % (timestamp, damage_name, int(amount), target)

  def FinalizeChart(self, x_axis, y_axis, accumulated):
    sys.path.append("/export/homedirs/chip/ChartDirector/lib")

    import pychartdir

    if x_axis[-1] - x_axis[0] < 60: return

    total = sum(accumulated.values())
    ordered = sorted(((damage, victim) for victim, damage in accumulated.items()), reverse=True)
    target = self._uid_map[ordered[0][1]]

    filename = time.strftime("%Y%m%d%H%M%S-raiddps", time.localtime(x_axis[0]))
    filename += "-%s-%d.png" % (target.lower(), int(total))
    def time_label(t, t0=x_axis[0]):
      return "%d:%02d" % (int((t - t0) / 60), int(t - t0) % 60)
    c = pychartdir.XYChart(800, 500)
    c.setPlotArea(60, 20, 700, 420)
    layer = c.addLineLayer2()
    layer.addDataSet(y_axis, -1, "Raid DPS")
    layer.setXData(x_axis)

    avg = int(total / (x_axis[-1] - x_axis[0]))
    c.yAxis().addMark(avg, 0x800080, "%d dps" % avg).setLineWidth(2)

    current_heroism_depth = 0
    last_heroism_timestamp = x_axis[0]
    for timestamp, delta in self._heroism_events:
      if x_axis[0] <= timestamp <= x_axis[-1]:
        current_heroism_depth += delta
        print "HERO: %s %d %d" % (timestamp, delta, current_heroism_depth)
        if current_heroism_depth > 0:
          c.xAxis().addZone(0, 1000, 0xffcccc)
        last_heroism_timestamp = timestamp

    labels = list()
    t = x_axis[0]
    labels.append(time_label(x_axis[0]))
    increment = (x_axis[-1] - x_axis[0]) / 25
    while t + increment < x_axis[-1]:
      t += increment
      labels.append(time_label(t))
      
    labels.append(time_label(x_axis[-1]))

    c.xAxis().setLinearScale2(x_axis[0], x_axis[-1], labels)
    c.xAxis().setLabelStyle("normal", 8, 0, 90)
    c.makeChart(filename)


class FreyaSunbeamSink(EventSink):
  mode_name = "sunbeams"
  mode_desc = "Freya Sunbeam reaction time"

  def __init__(self):
    EventSink.__init__(self, *("SPELL_AURA_APPLIED SPELL_AURA_REMOVED SPELL_PERIODIC_DAMAGE".split()))
    self._player_aura_applied = dict()  # player_name -> dict(src_id => time_applied)
    self._player_time_events = dict() # player_name -> [ time time time ... ]

  def Feed(self, event):
    if event.DestIsRaidMember() and event.spell_name == "Unstable Energy":
      if event.type == "SPELL_AURA_APPLIED":
        player_dict = self._player_aura_applied.get(event.dest_name)
        if player_dict is None:
          player_dict = self._player_aura_applied[event.dest_name] = dict()
        player_dict[event.source_id] = event.epoch_time
      elif event.type == "SPELL_PERIODIC_DAMAGE" or event.type == "SPELL_AURA_REMOVED":
        apply_time = self._player_aura_applied.get(event.dest_name, None)
        if apply_time is not None:
          apply_time = apply_time.get(event.source_id)
          print "Reaction time for %s: %.2f" % (event.dest_name, event.epoch_time - apply_time)
          self._player_time_events.setdefault(event.dest_name, []).append(event.epoch_time - apply_time)

  def Finalize(self):
    for player, timings in sorted(self._player_time_events.items()):
      mean = sum(timings) / len(timings)
      print "%s: %.2f" % (player, mean)

class AnubTimers(EventSink):
  mode_name = "anubtimers"
  mode_desc = "Anub'arak Event Timers"

  def __init__(self):
    EventSink.__init__(self)
    self._last_anub_damage = None
    self._anub_engage = None
    self._last_anub_pursue = list()
    self._first_pursue = None
    self._last_burrows = collections.defaultdict(int)  # id -> epoch time
    self._last_strike = collections.defaultdict(int) # id -> epoch time
    self._timelines = list()  # above reset per fight

  def Feed(self, event):
    if event.suffix == "DAMAGE" and event.dest_name == "Anub'arak":
      if not self._last_anub_damage or (event.epoch_time - self._last_anub_damage.epoch_time > 120):
        print "engage at %s" % event.PrettyTimestamp()
        if self._last_anub_pursue:
          self._timelines.append((self._anub_engage, self._last_anub_pursue,
                                  self._last_burrows, self._last_strike,
                                  self._first_pursue))
        self._anub_engage = event
        self._last_strike = collections.defaultdict(int)
        self._last_burrows = collections.defaultdict(int)
        self._last_anub_pursue = list()
        self._first_pursue = None
      self._last_anub_damage = event

    if event.type == 'SPELL_CAST_START':
      if event.spell_name == 'Shadow Strike':
        print "strike attempt at %s" % event.PrettyTimestamp()
        self._last_strike[event.source_id] = event.epoch_time
      elif event.spell_name == 'Submerge':
        print "submerge attempt at %s" % event.PrettyTimestamp()
        self._last_burrows[event.source_id] = event.epoch_time
    if event.type == "SPELL_CAST_SUCCESS" and event.spell_name == "Pursued by Anub'arak":
      self._last_anub_pursue.append(event.epoch_time)
      if not self._first_pursue:
        print "FIRST pursue at %s" % event.PrettyTimestamp()
        self._first_pursue = event
      else:
        print "pursue at %s" % event.PrettyTimestamp()

  def Finalize(self):
    if self._last_anub_pursue:
      self._timelines.append((self._anub_engage, self._last_anub_pursue,
                              self._last_burrows, self._last_strike,
                              self._first_pursue))

    for anub_damage, anub_pursue, burrow_times_d, strike_times_d, first_pursue in self._timelines:
      burrow_times = sorted((v, "submerge", k) for k, v in burrow_times_d.items())
      strike_times = sorted((v, "strike", k) for k, v in strike_times_d.items())
      pursue_times = sorted((v, "pursue", None) for v in anub_pursue)
      merged = sorted(burrow_times + strike_times + pursue_times)

      burrower_ids = dict()
      next_id = 1
      for t, a, actor in merged:
        if actor not in burrower_ids:
          burrower_ids[actor] = next_id
          next_id += 1

      print "Anub engage: %s" % anub_damage.PrettyTimestamp()
      for action_time, action, actor in merged:
        extra = ""
        if first_pursue and action == "strike":
          extra = " (since first pursue: %s)" % (action_time - first_pursue.epoch_time)
        if action == "pursue":
          print "  Anub pursue: %.2f" % (action_time - anub_damage.epoch_time)
        else:
          print "  Burrower %s %s: %.2f%s" % (burrower_ids[actor], action, action_time - anub_damage.epoch_time, extra)
      

class YoggPortalSink(EventSink):
  mode_name = "portals"
  mode_desc = "Yogg-Saron Portal Times"

  def __init__(self):
    EventSink.__init__(self, "SPELL_AURA_APPLIED")
    self.fast_filters.add("Illusion Room")
    self._portal_times = list()  # list of dict of player->delta
    self._current_portal_times = None
    self._last_portal = 0

  def Feed(self, event):
    if event.spell_name == "Illusion Room":
      if event.epoch_time - self._last_portal > 55:
        if self._current_portal_times:
          self._portal_times.append((self._last_portal, self._current_portal_times))
        self._current_portal_times = dict()
        self._last_portal = event.epoch_time

      self._current_portal_times[event.source_name] = event.epoch_time - self._last_portal

  def Finalize(self):
    if self._current_portal_times:
      self._portal_times.append((self._last_portal, self._current_portal_times))

    print "Per-portal timings:"
    player_averages = collections.defaultdict(list)  # player -> list
    for idx, portal_tuple in enumerate(self._portal_times):
      portal_when, portal_times = portal_tuple
      import time
      pretty = time.strftime("%H:%M:%S", time.localtime(portal_when))
      print "Portal %d (%s)" % (idx + 1, pretty)
      for player, time in portal_times.items():
        player_averages[player].append(time)
        print "  %.02f: %s" % (time, player)

    print
    print "Overall portal performance:"
    for player, times in sorted(player_averages.items()):
      times.sort()
      times = times[:-1]
      print "  %s: %.2f (%d portals)" % (player, sum(times) / max(1, len(times)), len(times))
      

class ArdentDefenderSink(EventSink):
  mode_name = "ardent"
  mode_desc = "Ardent Defender Proc"

  def __init__(self):
    EventSink.__init__(self, "SPELL_AURA_APPLIED", "UNIT_DIED")
    self._last_proc = collections.defaultdict(int)  # player -> last ardent proc event
    self._saves = list()

  def Feed(self, event):
    if event.type == 'SPELL_AURA_APPLIED':
      if event.spell_name == 'Ardent Defender':
        self._last_proc[event.source_name] = event

    proc = self._last_proc.get(event.dest_name, None)
    if proc:
      if event.epoch_time - proc.epoch_time > 10:
        self._saves.append((proc.dest_name, proc))
        del self._last_proc[proc.dest_name]

  def Finalize(self):
    save_count = collections.defaultdict(int)
    for player, save in sorted(self._saves):
      print "%s: %s" % (save.PrettyTimestamp(), player)
      save_count[player] += 1

    print
    print "Total saves:"
    for player, saves in sorted(save_count.items()):
      print "  %s: %d" % (player, saves)

class ValkyrShadowguardSink(EventSink):
  mode_name = 'lichking_valkyr'
  mode_desc = "Analysis of Val'kyr Shadowguard handling"

  def __init__(self):
    EventSink.__init__(self, "SPELL_SUMMON", "UNIT_DIED")
    self.fast_filters.add("Val'kyr Shadowguard")
    self._current_valk = dict()
    self._batches = list()
    self._last_summon = 0

  def Feed(self, event):
    if event.type == 'SPELL_SUMMON' and event.dest_name == "Val'kyr Shadowguard":
      if self._current_valk and event.epoch_time - self._last_summon > 35:
        self._Flush()
      self._current_valk[event.dest_id] = [ event, None ]
      self._last_summon = event.epoch_time
    if event.type == 'UNIT_DIED' and event.dest_name == "Val'kyr Shadowguard":
      if event.dest_id not in self._current_valk:
        print "ERROR: lil lost val'kyr: %s" % event.dest_id
      else:
        self._current_valk[event.dest_id][1] = event

  def _Flush(self):
    if not self._current_valk: return
    self._batches.append(self._current_valk.values())
    self._current_valk = dict()

  def Finalize(self):
    self._Flush()
    n = 1
    symbol_names = [ "triangle", "diamond", "circle" ]
    for batch in self._batches:
      print "Val'kyr batch %d:" % n
      n += 1
      idx = 0
      for events in batch:
        birth, death = events
        if death == None:
          result_str = "not killed"
        else:
          result_str = "killed in %.2f seconds" % (death.epoch_time - birth.epoch_time)
        print "  Val'kyr %d (%s): %s" % (idx + 1, symbol_names[idx], result_str)
        idx += 1

class SaurfangSink(EventSink):
  mode_name = 'saurfang'
  mode_desc = "Analysis of Saurfang Blood Power"

  def __init__(self):
    EventSink.__init__(self)
    self.last_damage_event = 0
    self.first_blood_power = None
    self.first_combat_event = None
    self.mark_events = list()
    self.attempt = 1
    self._Reset()

  def _Reset(self):
    self.boil_ticks = 0
    self.absorbed_boil_ticks = 0
    self.mark_ticks = 0
    self.nova_ticks = 0
    self.rune_ticks = 0
    self.melee_hits = 0
    self.beast_hits = 0
    self.melee_misses = 0
    self.has_blood_power = False

  def Feed(self, event):
    if event.prefix == 'SPELL' and event.spell_name == 'Blood Power':
      if event.type == 'SPELL_AURA_APPLIED':
        if self.first_blood_power is None:
          self.first_blood_power = event
          self._Reset()
        self.has_blood_power = True
      elif event.type == 'SPELL_AURA_REMOVED':
        self.has_blood_power = False

    if event.suffix == 'DAMAGE':
      if "Deathbringer Saurfang" in (event.dest_name, event.source_name):
        if (event.epoch_time - self.last_damage_event) > 90:
          self._Flush()
          self.first_combat_event = event
        self.last_damage_event = event.epoch_time

    if event.type == 'SPELL_CAST_START' and event.spell_name == 'Mark of the Fallen Champion':
      self._FlushMark(event)
    if self.has_blood_power:
      if event.prefix == "SPELL" and event.spell_name == 'Mark of the Fallen Champion':
        if event.IsEffectiveDamage():
          self.mark_ticks += 1
      if event.source_name == 'Deathbringer Saurfang':
        if event.prefix == 'SPELL' and event.IsEffectiveDamage():
          if event.spell_name == 'Boiling Blood':
            if hasattr(event, "missed_reason"):
              self.absorbed_boil_ticks += 1
            else:
              self.boil_ticks += 1
          elif event.spell_name == 'Blood Nova':
            self.nova_ticks += 1
          elif event.spell_name == 'Rune of Blood':
            self.rune_ticks += 1
        elif event.type == 'SWING_DAMAGE':
          self.melee_hits += 1
        elif event.type == 'SWING_MISSED':
          if event.missed_reason == 'ABSORB':
            self.melee_hits += 1
          else:
            self.melee_misses += 1
      if self.has_blood_power and event.source_name == 'Blood Beast':
        if event.IsEffectiveDamage():
          self.beast_hits += 1

  def _FlushMark(self, event):
    data = [ self.boil_ticks, self.absorbed_boil_ticks, self.mark_ticks, self.nova_ticks, self.rune_ticks, self.melee_hits, self.melee_misses, self.beast_hits ]
    self.mark_events.append((event, data))
    self._Reset()

  def _Flush(self):
    LABELS = "damaging_boil_ticks absorbed_boil_ticks mark_ticks nova_ticks rune_ticks melee_hits melee_misses beast_hits".split()
    if not self.mark_events: return
    print "%s: Attempt %d begins!" % (self.first_combat_event.PrettyTimestamp(), self.attempt)
    self.attempt += 1
    print "%s: First Blood Power!" % self.first_blood_power.PrettyTimestamp()
    last_event_or_blood_power = self.first_blood_power.epoch_time
    for event, values in self.mark_events:
      delta = event.epoch_time - last_event_or_blood_power
      fight_delta = event.epoch_time - self.first_combat_event.epoch_time
      print "%s: Mark cast begins (%.2f seconds from last, %.2f seconds into fight)" % (event.PrettyTimestamp(), delta, fight_delta)
      for label, num in zip(LABELS, values):
        print "  %s: %d" % (label, num)
      last_event_or_blood_power = event.epoch_time
    self._Reset()
    self.mark_events = list()
    self.first_blood_power = None
    print
    print

  def Finalize(self):
    pass


class DeathwhisperGhostSink(EventSink):
  mode_name = 'ldwghost'
  mode_desc = "Which ghosts hit whom"

  class Ghost(object):
    pass

  def __init__(self):
    EventSink.__init__(self)
    self.fast_filters.add("Vengeful Shade")
    def ghost_maker():
      ret = DeathwhisperGhostSink.Ghost()
      ret.spawn = None
      ret.melee_victim = None
      ret.melee_hit_time = None
      ret.explosion_victims = list()
      ret.explosion_hit_time = None
      return ret
    self.ghosts = collections.defaultdict(ghost_maker)

  def Feed(self, event):
    if event.type == "SPELL_SUMMON":
      ghost = self.ghosts[event.dest_id]
      ghost.spawn = event.PrettyTimestamp()
      ghost.spawn_epoch_time = event.epoch_time
    if event.prefix == "SWING":
      ghost = self.ghosts[event.source_id]
      ghost.melee_victim = event.dest_name
      ghost.melee_hit_time = event.PrettyTimestamp()
    if event.type == "SPELL_DAMAGE" or event.type == "SPELL_MISSED":
      ghost = self.ghosts[event.source_id]
      ghost.explosion_victims.append(event.dest_name)
      ghost.explosion_hit_time = event.PrettyTimestamp()

  def Finalize(self):
    ghosts = self.ghosts.values()
    ghosts.sort(lambda a, b: cmp(a.spawn, b.spawn))
    last_batch = 0
    batch_number = 1
    for ghost in ghosts:
      if ghost.spawn_epoch_time - last_batch > 5:
        print
        print "=== [ Ghost Summon %d ] ===" % (batch_number)
        last_batch = ghost.spawn_epoch_time
        batch_number += 1
      if ghost.melee_victim:
        print "%s - ghost spawned" % ghost.spawn
        print "%s -   melee hit: %s" % (ghost.melee_hit_time, ghost.melee_victim)
        print "%s -   explosion hit: %s" % (ghost.explosion_hit_time, ", ".join(sorted(ghost.explosion_victims)))
      else:
        print "%s - ghost spawned and avoided!  good work!" % ghost.spawn


class DamageCapSink(EventSink):
  mode_name = 'damage-cap'
  mode_desc = "Cap damage to specific mobs by suppressing events"

  @classmethod
  def RegisterOptions(self, parser):
    parser.add_option("", "--mob-name", help="name of mob to limit damage to")
    parser.add_option("", "--damage", help="discard events after this much damage is dealt", type=int)

  def __init__(self):
    EventSink.__init__(self)
    self.damage = collections.defaultdict(int)

  def Feed(self, event):
    if event.suffix == 'DAMAGE' and event.dest_name == self.options.mob_name:
      if self.damage[event.dest_id] > self.options.damage:
        return

      self.damage[event.dest_id] += event.amount
    print event.raw_line
